"""
This file provides a mechanism to parse Makefiles for the purpose of getting
access to variables contained therein
"""

import os
import re
import platform
import struct

_ifdef = re.compile("^ifdef (\w+)")
_ifndef = re.compile("^ifndef (\w+)")
_dependency = re.compile("^([^:]*):(.*)$")
_immediate_assignment = re.compile("^([A-Za-z]\w*)\s*:=(.*)$")
_append_assignment = re.compile("^([A-Za-z]\w*)\s*\+=(.*)$")
_deferred_assignment = re.compile("^([A-Za-z]\w*)\s*=(.*)$")

def parse_makefile(file_path):
    """Parse the given makefile and return a dictionary of variables."""
    with open(file_path) as open_file:
        return _parse_makefile_from_open_file(open_file)

def _parse_makefile_from_open_file(open_file):
    variables = {}

    # Pre-populate with some known defines
    for name in {
            "Linux": ("LINUX",),
            "Windows": ("WINDOWS_NT",),
            "Darwin": ("DARWIN", "MBSD", "MBSD_COCOA")}[platform.system()]:
        variables[name] = "1"

    if struct.calcsize("P") == 8:
        variables["AMD64"] = "1"

    if_stack = []
    for line in _interesting_iterator(open_file):
        # Handle skipping over sections of an if statement.  We're looking for
        # the contents of if_stack[-1].
        if if_stack:
            if if_stack[-1] == "else/endif":
                # We're supposed to skip until we get an else or an endif.
                if line.startswith("else"):
                    if_stack.pop()
                    # Start processing the "else" part of an if block.
                    if_stack.append("else")
                elif line.startswith("endif"):
                    if_stack.pop()

                # Keep skipping lines.
                continue

            if if_stack[-1] == "endif":
                # We're supposed to skip until we get an endif.
                if line.startswith("endif"):
                    if_stack.pop()

                # If we didn't get an endif, we'll keep looking for one.
                # Otherwise, we'll look for what's on the top of the stack.
                continue

            if if_stack[-1] == "if":
                # We're processing the "true" part of an if block and not
                # skipping anything..
                if line.startswith("else"):
                    if_stack.pop()

                    # Begin looking for endif, skipping any lines until then.
                    if_stack.append("endif")
                    continue

                if line.startswith("endif"):
                    # We're done this if statement
                    if_stack.pop()
                    continue

# TODO: Don't we need this?  Create a test that does this.  Check for entries
#       still existing on the stack.
#            if if_stack[-1] == "else":
#                # We're processing the "else" part of an if block.
#                if line.startswith("endif"):
#                    if_stack.pop()


        # Handle ifdef.
        match = _ifdef.match(line)
        if match:
            if variables.get(match.group(1)) is None:
                # The name is not defined.  Skip until we encounter an 'else'
                # or an 'endif'.
                if_stack.append("else/endif")
            else:
                # Process the "true" part of an if block.
                if_stack.append("if")
            continue

        # TODO: Refactor this code.
        # Handle ifndef.
        match = _ifndef.match(line)
        if match:
            if variables.get(match.group(1)) is not None:
                # The name is defined.  Skip until we encounter an 'else' or an
                # 'endif'
                if_stack.append("else/endif")
            else:
                # Process these lines
                if_stack.append("if")
            continue

        assert not line.startswith("ifeq")

        # Handle immediate assignments.
        match = _immediate_assignment.match(line)
        if match:
            variables[match.group(1)] = _expand_vars(
                match.group(2), variables).strip()

        # Handle append assignments.
        match = _append_assignment.match(line)
        if match:
            existing_value = variables.get(match.group(1), "")
            variables[match.group(1)] = (existing_value +
                ' ' + _expand_vars(match.group(2), variables).strip())

        # Handle deferred assignments.
        match = _deferred_assignment.match(line)
        if match:
            variables[match.group(1)] = match.group(2).strip()

    # Expand out any variables.
    for key, value in variables.items():
        variables[key] = _expand_vars(value, variables).strip()

    return variables

_variable_exp = re.compile("(\$\(([A-Za-z][A-Za-z0-9_]*)\))")
def _expand_vars(string, variables):
    """Expands any make variable references in string."""
    match = _variable_exp.search(string)
    while match:
        string = (string[:match.start(1)] + variables.get(match.group(2), "") +
            string[match.end(1):])
        match = _variable_exp.search(string)
    return string

def _interesting_iterator(open_file):
    """File iterator that skips over blank lines and comments.  It also joins
    lines together when it encounters trailing backslashes.
    """
    buf = []
    for line in open_file:
        line = line.strip()
        if not line and buf:
            yield " ".join(buf)
            buf = []
            continue

        if not line or line[0] == "#":
            continue

        if line[-1] == '\\':
            buf.append(line[:-1].strip())
        else:
            if buf:
                buf.append(line)
                yield " ".join(buf)
                buf = []
            else:
                yield line

